﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TWiStEr.Math;
using Phys.Model;
using Phys.Model.Affectors;
using System.Xml;
using System.Reflection;
using System.ComponentModel;
using System.IO;
using System.Collections;
using System.Globalization;

namespace Phys.Loader {
	public class WorldLoader {
		public static WorldObject LoadXml(string path) {
			XmlDocument doc = new XmlDocument();
			doc.Load(path);
			foreach (XmlNode node in doc.DocumentElement.Cast<XmlNode>().Where(n => n.Name.Contains('.'))) {
				if (node.Name.EndsWith(".References")) {
					foreach (XmlNode oRef in node.ChildNodes) {
						references.Add(oRef.Attributes["Name"].Value, LoadObject<object>(oRef, "Name"));
					}
				} else {
					Console.WriteLine("TODO: implement dotted objects: " + node.Name);
				}
			}
			WorldObject world = LoadObject<WorldObject>(doc.DocumentElement);
			var start = DateTime.Now;
			foreach (IObject o in world.AllObjects) {
				o.World = world;
			}
			Console.WriteLine((DateTime.Now - start).TotalMilliseconds);
			return world;
		}

		static IDictionary<string, object> references = new Dictionary<string, object>();
		static IEnumerable<string> types = new[] { "Phys.Model.{0}Object", "Phys.Model.Affectors.{0}Affector" };
		private static T LoadObject<T>(XmlNode node, params string[] ignoredAttributes) {
			object o = null;
			foreach (string type in types) {
				if (o != null) break;
				o = GetObject(type, node, ignoredAttributes);
			}
			if (o is IObject) {
				IObject obj = (IObject)o;
				foreach (XmlNode childNode in node.ChildNodes) {
					if (childNode.Name == "AffectorRef") {
						object @ref;
						if (references.TryGetValue(childNode.Attributes["Name"].Value, out @ref)) {
							obj.Affectors.Add((IAffector)@ref);
						}
					} else {
						object child = LoadObject<object>(childNode);
						if (child is IAffector) obj.Affectors.Add((IAffector)child);
						if (obj is CompositeObject && child is IObject) ((CompositeObject)obj).Objects.Add((IObject)child);
					}
				}
			}
			return (T)o;
		}

		private static object GetObject(string format, XmlNode node, params string[] ignoredAttributes) {
			if (node.NodeType != XmlNodeType.Element) return null;
			Type t = Assembly.GetAssembly(typeof(WorldObject)).GetType(string.Format(format, node.Name), false, true);
			if (t == null) return null;
			object o = t.GetConstructor(Type.EmptyTypes).Invoke(null);
			SetAttributes(o, node, ignoredAttributes);
			return o;
		}

		private static void SetAttributes(object o, XmlNode node, params string[] ignoredAttributes) {
			foreach (XmlAttribute attr in node.Attributes.Cast<XmlAttribute>().Where(a => !ignoredAttributes.Contains(a.Name))) {
				Type valueT;
				Action<object> setter = GetSetter(o, attr.Name, out valueT);
				if (setter == null) {
					Console.Error.WriteLine(attr.Name + " cannot be set on " + node.Name);
					continue;
				}
				setter(TypeDescriptor.GetConverter(valueT).ConvertFromString(null, CultureInfo.InvariantCulture, attr.Value));
			}
		}

		private static Action<object> GetSetter(object o, string name, out Type valueType) {
			Action<object> setter = null;
			valueType = null;
			PropertyInfo p = o.GetType().GetProperty(name, BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public);
			if (p != null) {
				valueType = p.PropertyType;
				setter = value => p.SetValue(o, value, null);
			} else {
				FieldInfo f = o.GetType().GetField(name, BindingFlags.SetField | BindingFlags.Instance | BindingFlags.Public);
				if (f != null) {
					valueType = f.FieldType;
					setter = value => f.SetValue(o, value);
				}
			}
			return setter;
		}
		public static void SaveXML(string path, WorldObject world) {
			File.WriteAllText(path, ToXMLString(world, true));
		}
		public static string ToXMLString(WorldObject world, bool nonDefaultOnly) {
			MemoryStream mem = new MemoryStream();
			XmlWriter xml = new XmlTextWriter(mem, Encoding.UTF8) {
				Formatting = Formatting.Indented,
				Indentation = 1, IndentChar = '\t',
				QuoteChar = '"'
			};
			xml.WriteStartDocument();
			WriteObject(xml, world, nonDefaultOnly);
			xml.WriteEndDocument();
			xml.Close();
			return Encoding.UTF8.GetString(mem.ToArray());
		}

		private static void WriteObject(XmlWriter xml, IObject obj, bool nonDefaultOnly) {
			string objectName = obj.GetType().Name;
			xml.WriteStartElement(objectName.Remove(objectName.IndexOf("Object")));
			WriteAttributes(xml, obj, nonDefaultOnly);
			foreach (IAffector aff in obj.Affectors) {
				string affectorName = aff.GetType().Name;
				xml.WriteStartElement(affectorName.Remove(affectorName.IndexOf("Affector")));
				WriteAttributes(xml, aff, nonDefaultOnly);
				xml.WriteEndElement();
			}
			if (obj is CompositeObject) {
				foreach (IObject child in ((CompositeObject)obj).Objects) {
					WriteObject(xml, child, nonDefaultOnly);
				}
			}
			xml.WriteEndElement();
		}

		private static void WriteAttributes(XmlWriter xml, object obj, bool nonDefaultOnly) {
			Type t = obj.GetType();
			object defaultObj = obj.GetType().GetConstructor(Type.EmptyTypes).Invoke(null);
			foreach (FieldInfo attr in t.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.SetField)) {
				object value = attr.GetValue(obj);
				object defaultValue = attr.GetValue(defaultObj);
				if (!(value is IEnumerable)) {
					if (!(nonDefaultOnly && object.Equals(defaultValue, value))) {
						xml.WriteAttributeString(attr.Name, TypeDescriptor.GetConverter(attr.FieldType).ConvertToString(value));
					}
				} else {
					Console.WriteLine("Skipping list: " + attr.ReflectedType.FullName + "." + attr.Name);
				}
			}
			foreach (PropertyInfo attr in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty)) {
				object value = attr.GetValue(obj, null);
				object defaultValue = attr.GetValue(defaultObj, null);
				if (!(value is IEnumerable)) {
					if (!(nonDefaultOnly && object.Equals(defaultValue, value))) {
						xml.WriteAttributeString(attr.Name, TypeDescriptor.GetConverter(attr.PropertyType).ConvertToString(value));
					}
				} else {
				}
			}
		}
	}
}
